#!/usr/bin/env python
#-------------------------------------------------------------------------------
# Copyright (c) 2008 Andrew Coleman under the MIT License
# See COPYING at the root directory of this project.
#-------------------------------------------------------------------------------
# Standard Modules
import asynchat
import socket
# Project Modules
import conf
import output
import control

# Set up the module
irc_sockets = dict()
irc_sockets_del_queue = list()
are_closing_all = False
have_closed_all = False


#-------------------------------------------------------------------------------
#   Main Control Loop
#-------------------------------------------------------------------------------
def Loop ():
    """This is the main loop, controlling all the sockets"""
    # This is where we poll our sockets
    # count=1 makes sure there is no infinite looping going on
    asynchat.asyncore.loop(timeout=0.05, use_poll=False, map=None, count=1)

    # if we're shutting down the application, and there are no remaining sockets open
    if are_closing_all and len(irc_sockets) < 1:
        have_closed_all = True

    # Swing through connections
    for network_name, irc_socket in irc_sockets.items():
        # Check for ones needing deletion
        if irc_socket.state == "disconnected":
            irc_sockets_del_queue.append(network_name)
        # Autojoin channels and make the socket ready for normal use
        elif irc_socket.state == "initialized":
            autojoin_channels = conf.Settings["IrcNetworks"][irc_socket.network_name]["AutojoinChannels"]
            if type(autojoin_channels) == type(list()):
                for channel in autojoin_channels:
                    join_channel(irc_socket, channel)
            else:
                channel = autojoin_channels
                join_channel(irc_socket, channel)
            irc_socket.state = "ready"


    # Remove closed sockets detected during the parsing swing
    while len(irc_sockets_del_queue) > 1:
        network_name = irc_sockets_del_queue.pop(0)
        socket = irc_sockets[network_name]
        close(socket)
        del irc_sockets[network_name]

    if are_closing_all == True and have_closed_all == True:
        output.Push("Quitting...")
        exit()


#-------------------------------------------------------------------------------
#   IRC Socket
#-------------------------------------------------------------------------------
class IRCSocket(asynchat.async_chat):
    def __init__(self, network, network_name, port, nickname, ident, real_name):
        """Set up the irc socket"""
        # Parent's constructor
        asynchat.async_chat.__init__(self)
        # Buffer to store input before terminator is found
        self.raw_input_buffer = ""
        self.set_terminator("\r\n")
        self.state = "uninitialized"
        self.channels = list()

        # IRC state variables
        self.network = network
        self.network_name = network_name
        self.port = port
        self.nickname = nickname
        self.ident = ident
        self.real_name = real_name

        # Setup buffers if we're set to do so
        self.input_buffer = list()
        self.output_buffer = list()

        self.attempt_connection()

    def attempt_connection(self):
        # Time to connect!
        output.Push("Connecting to: "+self.network+":"+str(self.port))
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.connect((self.network, self.port))
        except socket.error, error_info:
            # error_info could be a tuple (errno, errmsg) or just a string errmsg
            if type(error_info) == type(tuple()):
                errmsg = error_info[1]
            else:
                errmsg = error_info
            output.Push("Failed to connect to: " + self.network + ":" + str(self.port) + " because: " + str(errmsg), "warning")
            self.state = "wait to retry"
        except socket.herror, (errno, errmsg):
            output.Push("Failed to connect to: " + self.network + ":" + str(self.port) + " because: " + str(errmsg), "warning")
            self.state = "wait to retry"
        except socket.gaierror, (errno, errmsg):
            output.Push("Failed to connect to: " + self.network + ":" + str(self.port) + " because: " + str(errmsg), "warning")
            self.state = "wait to retry"
        except socket.timeout, errmsg:
            output.Push("Failed to connect to: " + self.network + ":" + str(self.port) + " because: " + str(errmsg), "warning")
            self.state = "wait to retry"

    def collect_incoming_data(self, data):
        """Called whenever we have an indeterminate amount of data come in"""
        self.raw_input_buffer += data

    def found_terminator(self):
        """Set up the irc message, and parse it into being"""
        # Get the raw and clear the buffer
        self.raw = self.raw_input_buffer
        self.raw_input_buffer = ""

        # Print the line if debugging
        output.Push(self.network_name + "> " + self.raw, "debug")

        # Store the line
        self.input_buffer.append(self.raw)

        # Parse the line
        (prefix, command, parameters, trailing) = _parse(self.raw)

        #-----------------------------------------------------------------------
        #   Read Events are handled here!
        #-----------------------------------------------------------------------
        # respond to pings quickly!
        if command == "PING":
            send_pong(self, trailing)
        # detect end of MOTD (good time to enable channel joining!)
        elif command == "376":
            self.state = "initialized"
        elif command == "PRIVMSG":
            # someone said something in a channel
            if parameters[0] != self.nickname:
                if trailing.startswith(self.nickname, 0, len(self.nickname)) != True:
                    return
                channel = parameters[0]
                user_input = trailing[len(self.nickname):]
            # someone said something in our PM
            else:
                channel = None
                user_input = trailing
            # by this point, there's a user addressing us in a channel, or pming
            user, hostmask = prefix.split("!", 1)
            if control.check_auth(user):
                #---------------------------------------------------------------
                #   User Commands are handled here!
                #---------------------------------------------------------------
                if user_input.lower() == "shutdown":
                    quit_all()

    def handle_connect (self):
        """Hook called when a socket has connected"""
        output.Push("Connected to: " + self.network + ":" + str(self.port))
        self.state = "connected"
        if self.writable():
            send_nick(self, self.nickname)
            send_user(self)

    def handle_close (self):
        """Hook called when a socket closes"""
        output.Push("Closing connection to: " + self.network + ":" + str(self.port))
        self.state = "disconnected"


def send_raw(irc_socket, prefix, command, parameters):
    """Constructs a raw message given the parts of an IRC protocol message"""
    message = str()
    if prefix != None:
        message += ':' + prefix + ' '
    message += command + ' '
    if type(parameters) == type(list()) and len(parameters) > 0:
        # prefix : for the trailing parameter
        parameters[-1] = ':' + parameters[-1]
        # put spaces between the parameters, too.
        message += ' '.join(parameters) + '\r\n'
        output.Push(irc_socket.network_name + "< " + message[:-2],"debug")
        irc_socket.push(message)
    elif type(parameters) == type(str()) and len(parameters) > 0:
        message += ':' + parameters + '\r\n'
        output.Push(irc_socket.network_name + "< " + message[:-2],"debug")
        irc_socket.push(message)
    else:
        output.Push("send_raw() attempt with bad parameters: type(parameters) = " + str(type(parameters)) + "; len(parameters) = " + str(len(parameters)) + "; for server: " + irc_socket.network_name, "warning")

def send_pong(irc_socket, pingText):
    """Send pong reply to ping on irc_socket"""
    send_raw(irc_socket, None, "PONG", pingText)

def send_nick(irc_socket, new_nick=""):
    """Send new_nick to irc_socket"""
    if new_nick is not "":
        irc_socket.nickname = new_nick
    output.Push("Changing nick to: " + new_nick)
    send_raw(irc_socket, None, "NICK", irc_socket.nickname)

def send_user(irc_socket):
    """Send user info to irc_socket"""
    irc_socket.push("USER " + irc_socket.ident + " " + irc_socket.network + " " + irc_socket.network_name + " " + irc_socket.real_name + "\r\n" )
    send_raw(irc_socket, None, "USER", [irc_socket.ident, irc_socket.network, irc_socket.network_name, irc_socket.real_name])

def join_channel(irc_socket, channel):
    """Join channel on irc_socket"""
    output.Push("Joining channel: " + channel + " on " + irc_socket.network_name)
    send_raw(irc_socket, None, "JOIN", channel)

def send_message(irc_socket, channel, message):
    """Send message to irc_socket"""
    send_raw(irc_socket, None, "PRIVMSG", [channel, message])

def send_quit(irc_socket, quit_message=""):
    """Send quit message to the socket"""
    output.Push("Quitting server: " + irc_socket.network_name)
    if quit_message == "":
        quit_message = conf.Settings["Defaults"]["DefaultQuitMessage"]
    # trim the quit message to fit within 512
    if len(quit_message) > 504:
        quit_message = quit_message [:504]
    send_raw(irc_socket, None, "QUIT", quit_message)

def close(irc_socket):
    send_quit(irc_socket)
    irc_sockets_del_queue.append(irc_socket.network_name)

def quit_all():
    """We cannot run the engines at this speed any longer, captain! We must shut her down!"""
    are_closing_all = True
    for network_name, irc_socket in irc_sockets.items():
        close(irc_socket)

#-------------------------------------------------------------------------------
#   IRC Line Parser
#-------------------------------------------------------------------------------
def _parse(line):
    """Parse a raw IRC line into a tuple with all the labels correctly applied, for easier access."""
    # Temporary variables
    unparsed_parameters = ""

    # Setup the final parsed message parts
    prefix = ""
    command = ""
    parameters = list()
    trailing = ""

    # Check for a prefix
    if line[0] == ":":
        line = line.split(None,2)
        if line[0] != "" and line[1] != "" and line[2] != "":
            prefix = line[0].strip()[1:]
            command = line[1].strip()
            unparsed_parameters = line[2].strip()
        else:
            output.Push("Ignoring badly formed line: \"" + "".join(line) + "\"", "warning")
            return ("", "", [""], "")

    # Without a prefix...
    else:
        line = line.split(None,1)
        prefix = ""
        command = line[0].strip()
        unparsed_parameters = line[1].strip()

    # Iterate through until we have all the parameters in place, trailing being the last one.
    while unparsed_parameters != "":
        if unparsed_parameters[0] is ":":
            # Huzzah, we found a trailing parameter :D
            trailing = unparsed_parameters[1:]
            parameters.append(unparsed_parameters[1:])
            unparsed_parameters = ""
        else:
            # Grab and store this parameter
            split_parameters = unparsed_parameters.split(None,1)
            parameters.append(split_parameters[0])
            # There might not be a trailing parameter, gotta have a way out.
            if len(split_parameters) <= 1:
                unparsed_parameters = ""
            else:
                unparsed_parameters = split_parameters[1]

    return (prefix, command, parameters, trailing)

#-------------------------------------------------------------------------------
#   First run stuff
#-------------------------------------------------------------------------------
# Grab important stuff for setting up autojoin sockets
port = conf.Settings["Defaults"]["DefaultPort"]
nickname = conf.Settings["Defaults"]["DefaultNickname"]
ident = conf.Settings["Defaults"]["DefaultIdent"]
real_name = conf.Settings["Defaults"]["DefaultRealname"]
# Let the user know what's up.
output.Push("IRC Handler inited")
output.Push("\tport: "+str(port), "debug")
output.Push("\tnickname: "+str(nickname), "debug")
output.Push("\tident: "+str(ident), "debug")
output.Push("\treal_name: "+str(real_name), "debug")

# Autojoin!
for GroupName, GroupInfo in conf.Settings["IrcNetworks"].items():
    if GroupInfo["Autojoin"] != True:
        continue
    if len(GroupInfo["KnownHosts"]) < 1:
        output.Push("Attempted to autoconnect to "+GroupName+\
              "but there are no known hosts. Skipping.", "warning")
        continue
    if len(GroupInfo["Nicknames"]) < 1:
        output.Push("Attempted to autoconnect to "+GroupName+\
              "but there are no usable nicknames configured. Skipping.", "warning")
        continue
    # Set up customized settings of the network
    ConnectionInfo = GroupInfo["KnownHosts"][0]
    # Create and add socket to the global dict
    irc_sockets[GroupName] = IRCSocket(ConnectionInfo["Host"],
                                       GroupName,
                                       ConnectionInfo["Port"],
                                       GroupInfo["Nicknames"][0],
                                       GroupInfo["Ident"],
                                       GroupInfo["Realname"])
